/***************************************************************************
 * Copyright 2012-2013 TXT e-solutions SpA
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * This work was performed within the IoT_at_Work Project
 * and partially funded by the European Commission's
 * 7th Framework Programme under the research area ICT-2009.1.3
 * Internet of Things and enterprise environments.
 *
 * Authors:
 *      Salvatore Piccione (TXT e-solutions SpA)
 *
 * Contributors:
 *        Domenico Rotondi (TXT e-solutions SpA)
 **************************************************************************/
package it.txt.ens.client.impl.osgi;

import it.txt.ens.client.core.ENSClient;
import it.txt.ens.client.core.factory.ENSBrokerConnectionParametersFactory;
import it.txt.ens.client.core.factory.ENSClientFactory;
import it.txt.ens.core.ENSAuthzServiceConnectionParameters;
import it.txt.ens.core.ENSResource;
import it.txt.ens.core.KeystoreParameters;
import it.txt.ens.core.X509CertificateRetrievalParameters;
import it.txt.ens.core.factory.ENSAuthzServiceConnectionParametersFactory;
import it.txt.ens.core.factory.ENSResourceFactory;
import it.txt.ens.core.factory.KeystoreParametersFactory;
import it.txt.ens.core.factory.X509CertificateRetrievalParametersFactory;
import it.txt.ens.core.osgi.FilterAttributes;
import it.txt.ens.core.osgi.RegisteredServices;
import it.txt.ens.core.util.parser.ENSConnectionParametersDictionaryParser;
import it.txt.ens.core.util.parser.ENSResourceDictionaryParser;
import it.txt.ens.core.util.parser.KeystoreParametersDictionaryParser;
import it.txt.ens.core.util.parser.ParseException;
import it.txt.ens.core.util.parser.X509CertificateRetrievalParametersDictionaryParser;
import it.txt.ens.schema.request.factory.ENSAuthorisationRequestFactory;
import it.txt.ens.schema.response.factory.ENSAuthorisationResponseFactory;

import java.io.File;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Dictionary;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedServiceFactory;

/**
 * @author Salvatore Piccione (TXT e-solutions SpA - salvatore.piccione AT txtgroup.com)
 * @author Domenico Rotondi (TXT e-solutions SpA - domenico.rotondi AT txtgroup.com)
 *
 */
public abstract class BasicENSClientMSF<C extends ENSClient> implements ManagedServiceFactory {
    private static final Logger LOGGER = Logger.getLogger(BasicENSClientMSF.class.getName());
    private static final ResourceBundle MESSAGES = ResourceBundle.getBundle("resource-bundles/" + 
            BasicENSClientMSF.class.getSimpleName(), Locale.ROOT);
    private static final String DEFAULT_IMPLEMENTATION_ID = "default";
    private static final int SERVICES_TO_UNGET_COUNT = 4;
    
    private final ConcurrentMap<String, List<ServiceReference<?>>> serviceReferences;
    private final ConcurrentMap<String, C> clients;
    private final ConcurrentMap<String, ServiceRegistration<C>> serviceRegistrations;
    private final DateFormat dateFormatter;
    private final Class<C> registeredClass;
    protected final BundleContext context;
    
    protected BasicENSClientMSF (BundleContext context, Class<C> registeredClass) {
        serviceReferences = new ConcurrentHashMap<String, List<ServiceReference<?>>>();
        serviceRegistrations = new ConcurrentHashMap<String, ServiceRegistration<C>>();
        this.context = context;
        this.clients = new ConcurrentHashMap<String, C>();
        dateFormatter = new SimpleDateFormat(ENSClientFactory.DATE_TIME_FORMAT);
        this.registeredClass = registeredClass;
    }
    
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void updated(String pid, Dictionary properties)
            throws ConfigurationException {
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.log(Level.FINER, "Updating the instance " + pid + ". Properties container is" +
                ((properties == null) ? "" : " not") + " null");
            
//            if (properties != null) {
//                StringBuilder builder = new StringBuilder();
//                boolean isFirst = true;
//                java.util.Enumeration<String> keys = properties.keys();
//                String key;
//                while (keys.hasMoreElements()) {
//                    key = keys.nextElement();
//                    if (isFirst)
//                        isFirst = false;
//                    else
//                        builder.append("\n");
//                    builder.append(key);
//                    builder.append(": ");
//                    builder.append(properties.get(key));
//                }
//                LOGGER.log(Level.FINER, "Properties:\n" + builder.toString());
//            }
        }
        
        if (clients.containsKey(pid)) {
            String message = MessageFormat.format(MESSAGES.getString("updatingClientConfig"),pid);
            if (LOGGER.isLoggable(Level.WARNING))
                LOGGER.log(Level.WARNING, message);
            throw new ConfigurationException(null,message);
        }
        if (properties == null) {
            if (LOGGER.isLoggable(Level.FINE))
                LOGGER.log(Level.FINE, MessageFormat.format(MESSAGES.getString("nullConfig"),pid));
        }
        
        List<ServiceReference<?>> references = new ArrayList<ServiceReference<?>>(SERVICES_TO_UNGET_COUNT);
        
        String subjectID = (String) properties.get(ENSClientFactory.SUBJECT_ID_KEY);
        if (subjectID == null)
            throw new ConfigurationException(ENSClientFactory.SUBJECT_ID_KEY, "Missing property");

        ENSResource targetResource;
        String filter = "(" + FilterAttributes.ENS_RESOURCE_IMPLEMENTATION_KEY + "=" +
                DEFAULT_IMPLEMENTATION_ID + ")";
        //select the ENSResourceFactory default implementation
        try {
            Collection<ServiceReference<ENSResourceFactory>> serviceRefCollection = 
                    context.getServiceReferences(RegisteredServices.ENS_RESOURCE_FACTORY,filter);
            if (serviceRefCollection.isEmpty())
                throw new ConfigurationException(null, "No service found. Class = " + 
                        RegisteredServices.ENS_RESOURCE_FACTORY.getName() + " Filter = " + filter);
            ServiceReference<ENSResourceFactory> serviceRef = serviceRefCollection.iterator().next();
            targetResource = ENSResourceDictionaryParser.parse(context.getService(serviceRef),
                    properties);
            context.ungetService(serviceRef);
            
            //update the properties by updating all resource properties
            if (properties.get(ENSResourceFactory.URI) == null)
                properties.put(ENSResourceFactory.URI, targetResource.getURI().toString());
            if (properties.get(ENSResourceFactory.HOST) == null)
                properties.put(ENSResourceFactory.HOST, targetResource.getHost());
            if (properties.get(ENSResourceFactory.NAMESPACE) == null)
                properties.put(ENSResourceFactory.NAMESPACE, targetResource.getNamespace());
            if (properties.get(ENSResourceFactory.PATH) == null)
                properties.put(ENSResourceFactory.PATH, targetResource.getPath());
            if (properties.get(ENSResourceFactory.PATTERN) == null)
                properties.put(ENSResourceFactory.PATTERN, targetResource.getPattern());
            
        } catch (InvalidSyntaxException e) {
            throw new ConfigurationException(null,"Bad service filter: " + filter + 
                    " Service class = " + RegisteredServices.ENS_RESOURCE_FACTORY.getName(),e);
        } catch (IllegalArgumentException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ ENSResource.class.getName(), e);
        } catch (ParseException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ ENSResource.class.getName(), e);
        }
        
        ENSAuthzServiceConnectionParameters accessRequestBrokerConnParams;
        //select the ENSConnectionParametersFactory default implementation
        filter = "(" + FilterAttributes.ENS_AUTHZ_SERVICE_CONN_PARAMS_IMPLEMENTATION_KEY + 
                "=" + DEFAULT_IMPLEMENTATION_ID + ")";
        try {
            Collection<ServiceReference<ENSAuthzServiceConnectionParametersFactory>> serviceRefCollection = 
                    context.getServiceReferences(
                        RegisteredServices.ENS_AUTHZ_SERVICE_CONNECTION_PARAMS_FACTORY,filter);
            if (serviceRefCollection.isEmpty())
                throw new ConfigurationException(null, "No service found. Class = " + 
                    RegisteredServices.ENS_AUTHZ_SERVICE_CONNECTION_PARAMS_FACTORY.getName()
                    + " Filter = " + filter);
            ServiceReference<ENSAuthzServiceConnectionParametersFactory> serviceRef = serviceRefCollection.iterator().next();
            accessRequestBrokerConnParams = ENSConnectionParametersDictionaryParser.parse(
                    context.getService(serviceRef),
                    properties);
            context.ungetService(serviceRef);
        } catch (InvalidSyntaxException e) {
            throw new ConfigurationException(null,"Bad service filter: " + filter + 
                " Service class = " + RegisteredServices.ENS_AUTHZ_SERVICE_CONNECTION_PARAMS_FACTORY.getName(),e);
        } catch (IllegalArgumentException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ ENSAuthzServiceConnectionParameters.class.getName(), e);
        } catch (ParseException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ ENSAuthzServiceConnectionParameters.class.getName(), e);
        }

        KeystoreParameters keystoreParams;
        filter = "(" + FilterAttributes.KEYSTORE_PARAMETERS_FACTORY_IMPLEMENTATION_KEY + 
                "=" + DEFAULT_IMPLEMENTATION_ID + ")";
        try {
            Collection<ServiceReference<KeystoreParametersFactory>> serviceRefCollection = 
                    context.getServiceReferences(RegisteredServices.KEYSTORE_PARAMETERS_FACTORY,filter);
            if (serviceRefCollection.isEmpty())
                throw new ConfigurationException(null, "No service found. Class = " + 
                        RegisteredServices.KEYSTORE_PARAMETERS_FACTORY.getName() + " Filter = " + filter);
            ServiceReference<KeystoreParametersFactory> serviceRef = serviceRefCollection.iterator().next();
            keystoreParams = KeystoreParametersDictionaryParser.parse(
                    context.getService(serviceRef),
                    properties);
            context.ungetService(serviceRef);
        } catch (InvalidSyntaxException e) {
            throw new ConfigurationException(null,"Bad service filter: " + filter + 
                " Service class = " + RegisteredServices.KEYSTORE_PARAMETERS_FACTORY.getName(),e);
        } catch (IllegalArgumentException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ KeystoreParameters.class.getName(), e);
        } catch (ParseException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ KeystoreParameters.class.getName(), e);
        }
        
        X509CertificateRetrievalParameters certParams;
        filter = "(" + FilterAttributes.X509CER_RETRIEVAL_PARAMS_FACTORY_IMPLEMENTATION_KEY + 
                "=" + DEFAULT_IMPLEMENTATION_ID + ")";
        try {
            Collection<ServiceReference<X509CertificateRetrievalParametersFactory>> serviceRefCollection = 
                    context.getServiceReferences(
                        RegisteredServices.X509_CERTIFICATE_RETRIEVAL_PARAMS_FACTORY,filter);
            if (serviceRefCollection.isEmpty())
                throw new ConfigurationException(null, "No service found. Class = " + 
                    RegisteredServices.X509_CERTIFICATE_RETRIEVAL_PARAMS_FACTORY.getName() + " Filter = " + filter);
            ServiceReference<X509CertificateRetrievalParametersFactory> serviceRef = 
                    serviceRefCollection.iterator().next();
            certParams = X509CertificateRetrievalParametersDictionaryParser.parse(
                    context.getService(serviceRef),properties);
            context.ungetService(serviceRef);
        } catch (InvalidSyntaxException e) {
            throw new ConfigurationException(null,"Bad service filter: " + filter + 
                " Service class = " + X509CertificateRetrievalParametersFactory.class.getName(),e);
        } catch (IllegalArgumentException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ X509CertificateRetrievalParameters.class.getName(), e);
        } catch (ParseException e) {
            throw new ConfigurationException(null, "An error occurred while creating an " +
                "instance of "+ X509CertificateRetrievalParameters.class.getName(), e);
        }
        
        String capabilityDirectorySTR = (String) properties.get(ENSClientFactory.CAPABILITY_DIRECTORY_PATH_KEY);
        if (capabilityDirectorySTR == null)
            throw new ConfigurationException(ENSClientFactory.CAPABILITY_DIRECTORY_PATH_KEY,
                    "Missing property");
        File capabilityDirectory = new File (capabilityDirectorySTR);
        
        ENSBrokerConnectionParametersFactory connParamsFactory;
        filter = "(" + it.txt.ens.client.core.osgi.FilterAttributes.ENS_BROKER_CONN_PARAMS_FACTORY_IMPLEMENTATION_KEY + 
                "=" + DEFAULT_IMPLEMENTATION_ID + ")";
        try {
            Collection<ServiceReference<ENSBrokerConnectionParametersFactory>> serviceRefCollection = 
                context.getServiceReferences(
                    it.txt.ens.client.core.osgi.RegisteredServices.ENS_BROKER_CONN_PARAMS_FACTORY,filter);
            if (serviceRefCollection.isEmpty())
                throw new ConfigurationException(null, "No service found. Class = " + 
                        ENSBrokerConnectionParametersFactory.class.getName() + " Filter = " + filter);
            ServiceReference<ENSBrokerConnectionParametersFactory> serviceRef = serviceRefCollection.iterator().next();
            connParamsFactory = context.getService(serviceRef);
            references.add(serviceRef);
        } catch (InvalidSyntaxException e) {
            throw new ConfigurationException(null,"Bad service filter: " + filter + 
                " Service class = " + ENSBrokerConnectionParametersFactory.class.getName(),e);
        }
        
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.log(Level.FINER, "Searching for the " + ENSAuthorisationRequestFactory.class.getSimpleName());
        }
        ServiceReference<ENSAuthorisationRequestFactory> requestFactorySR = null;
        do {
            requestFactorySR = context.getServiceReference(
                it.txt.ens.schema.osgi.RegisteredServices.ENS_AUTHZ_REQUEST_FACTORY);
        } while (requestFactorySR == null);
        ENSAuthorisationRequestFactory requestFactory = context.getService(requestFactorySR);
        references.add(requestFactorySR);
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.log(Level.FINER, "End of the search for the " + ENSAuthorisationRequestFactory.class.getSimpleName());
        }
        
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.log(Level.FINER, "Searching for the " + ENSAuthorisationResponseFactory.class.getSimpleName());
        }
        ServiceReference<ENSAuthorisationResponseFactory> responseFactorySR = null;
        do {
            responseFactorySR = context.getServiceReference(
                it.txt.ens.schema.osgi.RegisteredServices.ENS_AUTHZ_RESPONSE_FACTORY);
        } while (responseFactorySR == null);
        ENSAuthorisationResponseFactory responseFactory = context.getService(responseFactorySR);
        references.add(responseFactorySR);
        if (LOGGER.isLoggable(Level.FINER)) {
            LOGGER.log(Level.FINER, "End of the search for the " + ENSAuthorisationResponseFactory.class.getSimpleName());
        }            
        serviceReferences.put(pid, references);
        
        Date preferredSessionExpiration;
        try {
            preferredSessionExpiration = dateFormatter.parse((String) properties.get(ENSClientFactory.PREFERRED_SESSION_EXPIRATION_KEY));
        } catch (java.text.ParseException e) {
            String message = "Bad preferred session expiration. Expected format: " +
                    ENSClientFactory.DATE_TIME_FORMAT;
                ConfigurationException ce = new ConfigurationException(
                        ENSClientFactory.PREFERRED_SESSION_EXPIRATION_KEY, message, e);
                LOGGER.log(Level.SEVERE, ce.getMessage());
                throw ce;
        }
        C client = createClient(pid, subjectID, targetResource, 
                accessRequestBrokerConnParams, keystoreParams, certParams, capabilityDirectory,
                preferredSessionExpiration, connParamsFactory, requestFactory, responseFactory);
        
        clients.put(pid, client);
        serviceRegistrations.put(pid, context.registerService(registeredClass, client, properties));
        
        //TODO DELETE
        StringBuilder builder = new StringBuilder();
        boolean isFirst = true;
        java.util.Enumeration<String> keys = properties.keys();
        String key;
        while (keys.hasMoreElements()) {
            key = keys.nextElement();
            if (isFirst)
                isFirst = false;
            else
                builder.append("\n");
            builder.append(key);
            builder.append(": ");
            builder.append(properties.get(key));
        }
        LOGGER.log(Level.FINER, "Registered an instance of " + client.getClass().getName() + 
                " with pid " + pid + " and the following properties:\n" + builder.toString());
    }
    
    protected abstract C createClient (String pid, String subjectID,
            ENSResource targetResource, ENSAuthzServiceConnectionParameters accessRequestBrokerConnParams,
            KeystoreParameters keystoreParams, X509CertificateRetrievalParameters certParams, 
            File capabilityDirectory,
            Date preferredSessionExpiration,
            ENSBrokerConnectionParametersFactory ensBrokerConnectionParametersFactory, 
            ENSAuthorisationRequestFactory requestFactory, ENSAuthorisationResponseFactory responseFactory);
    
    
    public void deleted(String pid) {
        List<ServiceReference<?>> references = serviceReferences.remove(pid);
        if (references != null) {
            for (ServiceReference<?> reference : references)
                context.ungetService(reference);
        }
        clients.remove(pid);
        serviceRegistrations.remove(pid);
        if (LOGGER.isLoggable(Level.INFO))
            LOGGER.log(Level.INFO, "Removed subscriber with pid " + pid);
    }
}
